package tracing

import (
	"fmt"
	"log"
	"os"

	"github.com/mehdihadeli/go-ecommerce-microservices/internal/pkg/config/environemnt"
	config2 "github.com/mehdihadeli/go-ecommerce-microservices/internal/pkg/otel/config"

	"emperror.dev/errors"
	"go.opentelemetry.io/contrib/propagators/ot"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/jaeger"
	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/exporters/zipkin"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	tracesdk "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"

	_ "go.opentelemetry.io/otel"
)

// https://opentelemetry.io/docs/reference/specification/
// https://opentelemetry.io/docs/instrumentation/go/getting-started/
// https://opentelemetry.io/docs/instrumentation/go/manual/
// https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/
// https://uptrace.dev/opentelemetry/go-tracing.html
// https://lightstep.com/blog/opentelemetry-go-all-you-need-to-know
// https://trstringer.com/otel-part2-instrumentation/
// https://trstringer.com/otel-part5-propagation/
// https://github.com/tedsuo/otel-go-basics/blob/main/server.go

type TracingOpenTelemetry struct {
	config         *config2.OpenTelemetryOptions
	jaegerExporter tracesdk.SpanExporter
	zipkinExporter tracesdk.SpanExporter
	stdExporter    tracesdk.SpanExporter
	environment    environemnt.Environment
	TracerProvider *tracesdk.TracerProvider
	AppTracer      AppTracer
}

// Create one tracer per package
// NOTE: You only need a tracer if you are creating your own spans

func NewOtelTracing(
	config *config2.OpenTelemetryOptions,
	environment environemnt.Environment,
) (*TracingOpenTelemetry, error) {
	openTel := &TracingOpenTelemetry{config: config, environment: environment}

	err := openTel.configExporters()
	if err != nil {
		return nil, errors.WrapIf(err, "error in config exporter")
	}

	// https://opentelemetry.io/docs/instrumentation/go/manual/#initializing-a-new-tracer
	err = openTel.configTracerProvider()
	if err != nil {
		return nil, err
	}

	// https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/propagators/ot/ot_propagator.go
	// https://github.com/open-telemetry/opentelemetry-go/blob/main/propagation/trace_context.go
	// https://github.com/open-telemetry/opentelemetry-go/blob/main/propagation/baggage.go/
	// https://trstringer.com/otel-part5-propagation/
	propagators := []propagation.TextMapPropagator{
		ot.OT{}, // should be placed before `TraceContext` for preventing conflict
		propagation.TraceContext{},
		propagation.Baggage{},
	}

	// Register our TracerProvider as the global so any imported
	// instrumentation in the future will default to using it.
	otel.SetTracerProvider(openTel.TracerProvider)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagators...))

	// https://trstringer.com/otel-part2-instrumentation/
	// Finally, set the tracer that can be used for this package. global app tracer
	openTel.AppTracer = NewAppTracer(config.InstrumentationName)

	return openTel, nil
}

func (o *TracingOpenTelemetry) configTracerProvider() error {
	var sampler tracesdk.Sampler
	if o.config.AlwaysOnSampler {
		sampler = tracesdk.AlwaysSample()
	} else {
		sampler = tracesdk.NeverSample()
	}

	// https://github.com/open-telemetry/opentelemetry-go/blob/main/example/fib/main.go#L44
	// Ensure default SDK resources and the required service name are set.
	r, err := resource.Merge(
		resource.Default(),
		resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String(o.config.ServiceName),
			attribute.Int64("ID", o.config.Id),
			attribute.String("environment", o.environment.GetEnvironmentName()),
		),
	)
	if err != nil {
		return err
	}

	tp := tracesdk.NewTracerProvider(
		// Always be sure to batch in production.
		tracesdk.WithBatcher(o.jaegerExporter),
		tracesdk.WithBatcher(o.zipkinExporter),
		tracesdk.WithBatcher(o.stdExporter),
		tracesdk.WithSampler(sampler),

		// https://opentelemetry.io/docs/instrumentation/go/exporting_data/#resources
		// Resources are a special type of attribute that apply to all spans generated by a process
		tracesdk.WithResource(r),
	)
	o.TracerProvider = tp
	return nil
}

func (o *TracingOpenTelemetry) configExporters() error {
	logger := log.New(os.Stderr, "otel_log", log.Ldate|log.Ltime|log.Llongfile)

	if o.config.JaegerExporterOptions != nil {
		// Create the Jaeger exporter
		jaegerExporter, err := jaeger.New(jaeger.WithAgentEndpoint(
			jaeger.WithAgentHost(o.config.JaegerExporterOptions.AgentHost),
			jaeger.WithAgentPort(o.config.JaegerExporterOptions.AgentPort),
			jaeger.WithLogger(logger),
		))
		if err != nil {
			return err
		}
		o.jaegerExporter = jaegerExporter
	}
	if o.config.ZipkinExporterOptions != nil {
		zipkinExporter, err := zipkin.New(
			o.config.ZipkinExporterOptions.Url,
		)
		if err != nil {
			return err
		}

		o.zipkinExporter = zipkinExporter
	}
	if o.config.UseStdout {
		stdExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
		if err != nil {
			return fmt.Errorf("creating stdout exporter: %w", err)
		}
		o.stdExporter = stdExporter
	}

	return nil
}
